package org.xx.armory.commons;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.xx.armory.commons.Validators.notBlank;
import static org.xx.armory.commons.Validators.notNull;

/**
 * 用于实现加密、解密和哈希算法的工具类。
 *
 * @author Haart
 * @see javax.crypto.Cipher
 * @see java.security.MessageDigest
 */
public final class CryptographicUtils {
    private static final String ENCRYPT_DECRYPT_ALGORITHM = "DES/ECB/PKCS5Padding";
    private static final String HASH_ALGORITHM = "SHA-1";

    @SuppressWarnings("unused")
    private static final Logger LOGGER = LoggerFactory.getLogger(CryptographicUtils.class);

    /**
     * 加密算法使用的随机数。
     * <p>{@link SecureRandom}类型是线程安全的，所以可以共用。</p>
     */
    private static final SecureRandom RANDOM = new SecureRandom();

    private CryptographicUtils() {
        throw new UnsupportedOperationException();
    }

    /**
     * 对字符串按照{@literal UTF_8}编码，然后加密, 并将加密结果编码为Base64格式
     *
     * @param s
     *         待加密的字符串
     * @param key
     *         用于加密的密钥。
     * @return 已加密的字符串
     * @throws CryptographicException
     *         如果加密过程中出现错误
     * @throws UncheckedIOException
     *         如果将字符串转为字节流或者将字节流转为字符串时出现错误。
     * @see #encryptWithBase64(String, Charset, SecretKey)
     */
    public static String encryptWithBase64(
            String s,
            SecretKey key
    ) {
        return encryptWithBase64(s, UTF_8, key);
    }

    /**
     * 对Base64格式字符串进行解密, 并将解密的结果按照{@literal UTF_8}编码为字符串
     *
     * @param s
     *         待解密的Base64格式字符串
     * @param key
     *         用于解密的密钥。
     * @return 已解密的字符串
     * @throws CryptographicException
     *         如果加密过程中出现错误
     * @throws UncheckedIOException
     *         如果将字符串转为字节流或者将字节流转为字符串时出现错误。
     * @see #decryptWithBase64(String, Charset, SecretKey)
     */
    public static String decryptWithBase64(
            String s,
            SecretKey key
    ) {
        return decryptWithBase64(s, UTF_8, key);
    }

    /**
     * 对字符串按照指定的编码格式编码，然后加密, 并将加密结果编码为Base64格式
     *
     * @param s
     *         待加密的字符串
     * @param charset
     *         将文本转化为字节的编码。
     * @param key
     *         用于加密的密钥。
     * @return 已加密的字符串
     * @throws CryptographicException
     *         如果加密过程中出现错误
     * @throws UncheckedIOException
     *         如果将字符串转为字节流或者将字节流转为字符串时出现错误。
     * @see #decryptWithBase64(String, Charset, SecretKey)
     * @see #encrypt(InputStream, OutputStream, SecretKey)
     */
    public static String encryptWithBase64(
            String s,
            Charset charset,
            SecretKey key
    ) {
        if (s == null || s.isEmpty()) {
            return "";
        } else {
            try (final ByteArrayInputStream src = new ByteArrayInputStream(s.getBytes(charset));
                 final ByteArrayOutputStream dst = new ByteArrayOutputStream()) {
                encrypt(src, dst, key);
                return Base64.getEncoder().encodeToString(dst.toByteArray());
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }
    }

    /**
     * 对Base64格式字符串进行解密, 并将解密的结果按照指定编码为字符串
     *
     * @param s
     *         待解密的Base64格式字符串
     * @param charset
     *         将文本转化为字节的编码。
     * @param key
     *         用于解密的密钥。
     * @return 已解密的字符串
     * @throws CryptographicException
     *         如果解密过程中出现错误。
     * @throws UncheckedIOException
     *         如果将字符串转为字节流或者将字节流转为字符串时出现错误。
     * @see #encryptWithBase64(String, Charset, SecretKey)
     * @see #decrypt(InputStream, OutputStream, SecretKey)
     */
    public static String decryptWithBase64(
            String s,
            Charset charset,
            SecretKey key
    ) {
        if (s == null || s.isEmpty()) {
            return "";
        } else {
            final byte[] bytes = Base64.getDecoder().decode(s);
            try (final ByteArrayInputStream src = new ByteArrayInputStream(bytes);
                 final ByteArrayOutputStream dst = new ByteArrayOutputStream()) {
                decrypt(src, dst, key);
                return new String(dst.toByteArray(), charset);
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }
    }

    /**
     * 使用密钥对指定的输入流进行加密, 并将结果写入指定的输出流。
     *
     * <p>对于DES算法，默认模式为{@literal ECB/PCKS5Padding}</p>
     *
     * @param src
     *         指定的输入流。
     * @param dst
     *         指定的输出流。
     * @param key
     *         用于加密的密钥。
     * @throws UncheckedIOException
     *         如果读取流或者写入流过程中出现错误。
     * @throws IllegalStateException
     *         如果加密算法不存在。
     * @throws CryptographicException
     *         如果加密过程中出现错误
     * @throws IllegalArgumentException
     *         如果参数{@code src}是{@code null}或者{@code dst}或者{@code key}是{@code null}或者如果参数{@code key}表示不正确的密钥。
     */
    public static void encrypt(
            InputStream src,
            OutputStream dst,
            SecretKey key
    ) {
        notNull(src, "src");
        notNull(dst, "dst");
        notNull(key, "key");

        final int BUFFER_SIZE = 128;

        final Cipher cipher;
        try {
            cipher = Cipher.getInstance(ENCRYPT_DECRYPT_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, key, RANDOM);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
            throw new IllegalStateException(ex);
        } catch (InvalidKeyException ex) {
            throw new IllegalArgumentException("illegal key", ex);
        }

        try {
            final byte[] buf = new byte[BUFFER_SIZE];
            int rl = src.read(buf);
            while (rl != -1) {
                dst.write(cipher.update(buf, 0, rl));
                rl = src.read(buf);
            }

            try {
                dst.write(cipher.doFinal());
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            } catch (GeneralSecurityException ex) {
                throw new CryptographicException(ex);
            }

            dst.flush();
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    /**
     * 使用唯一密钥对指定的输入流进行解密, 并将结果写入指定的输出流。
     *
     * @param src
     *         指定的输入流。
     * @param dst
     *         指定的输出流。
     * @param key
     *         用于解密的密钥。
     * @throws UncheckedIOException
     *         如果读取流或者写入流过程中出现错误。
     * @throws IllegalStateException
     *         如果加密算法不存在。
     * @throws CryptographicException
     *         如果解密过程中出现错误。
     * @throws IllegalArgumentException
     *         如果参数{@code src}是{@code null}或者{@code dst}或者{@code key}是{@code null}或者如果参数{@code key}表示不正确的密钥。
     */
    public static void decrypt(
            InputStream src,
            OutputStream dst,
            SecretKey key
    ) {
        notNull(src, "src");
        notNull(dst, "dst");
        notNull(key, "key");

        final int BUFFER_SIZE = 128;

        final Cipher cipher;
        try {
            cipher = Cipher.getInstance(ENCRYPT_DECRYPT_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, key, RANDOM);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
            throw new IllegalStateException(ex);
        } catch (InvalidKeyException ex) {
            throw new IllegalArgumentException("illegal key", ex);
        }

        try {
            final byte[] buf = new byte[BUFFER_SIZE];
            int rl = src.read(buf);
            while (rl != -1) {
                dst.write(cipher.update(buf, 0, rl));
                rl = src.read(buf);
            }

            try {
                dst.write(cipher.doFinal());
            } catch (GeneralSecurityException ex) {
                throw new CryptographicException(ex);
            }

            dst.flush();
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    /**
     * 使用{@literal SHA-1}算法对指定的字节数组进行哈希。
     *
     * @param src
     *         待哈希的字节数组
     * @return 哈希的结果, 如果参数{@code src}是{@code null}, 那么相当于对长度为0的字节数组进行哈希。
     */
    public static byte[] hash(
            byte[] src
    ) {
        final MessageDigest md;
        try {
            md = MessageDigest.getInstance(HASH_ALGORITHM);
        } catch (GeneralSecurityException ex) {
            throw new IllegalStateException(ex);
        }

        if (src != null) {
            md.update(src);
            return md.digest();
        } else {
            return md.digest(new byte[0]);
        }
    }

    /**
     * 使用{@literal SHA-1}算法对指定的长整数进行哈希。
     *
     * @param l
     *         待哈希的长整数。
     * @return 哈希的结果。
     * @see #hash(byte[])
     */
    public static byte[] hash(
            long l
    ) {
        final byte[] src = new byte[]{
                (byte) (l & 0xFF), (byte) ((l >> 8) & 0xFF),
                (byte) ((l >> 16) & 0xFF), (byte) ((l >> 24) & 0xFF),
                (byte) ((l >> 32) & 0xFF), (byte) ((l >> 40) & 0xFF),
                (byte) ((l >> 48) & 0xFF), (byte) ((l >> 56) & 0xFF),};

        return hash(src);
    }

    /**
     * 使用{@literal SHA-1}算法对指定的整数进行哈希。
     *
     * @param i
     *         待哈希的整数。
     * @return 哈希的结果。
     * @see #hash(byte[])
     */
    public static byte[] hash(final int i) {
        final byte[] src = new byte[]{
                (byte) (i & 0xFF), (byte) ((i >> 8) & 0xFF),
                (byte) ((i >> 16) & 0xFF), (byte) ((i >> 24) & 0xFF)};

        return hash(src);
    }

    /**
     * 使用{@literal SHA-1}算法对指定的字符串进行哈希。
     * <p>
     * 首先将字符串按照参数{@code charset}转化为字节数组, 然后进行哈希。
     * </p>
     *
     * @param s
     *         待哈希的字符串。
     * @param charset
     *         字符串编码。
     * @return 哈希的结果。
     * @throws IllegalArgumentException
     *         如果参数{@code charset}是{@code null}。
     * @see String#getBytes()
     * @see #hash(byte[])
     */
    public static byte[] hash(
            String s,
            Charset charset
    ) {
        notNull(charset, "charset");

        if (s == null) {
            return hash((byte[]) null);
        }

        return hash(s.getBytes(charset));
    }

    /**
     * 使用{@literal SHA-1}算法对指定的字符串进行哈希。
     * <p>
     * 首先将字符串按照{@literal UTF-8}编码转化为字节数组, 然后进行哈希。
     * </p>
     *
     * @param s
     *         待哈希的字符串。
     * @return 哈希的结果。
     * @throws IllegalArgumentException
     *         如果参数{@code charset}是{@code null}。
     * @see #hash(String, Charset)
     */
    public static byte[] hash(
            String s
    ) {
        return hash(s, UTF_8);
    }

//    /**
//     * 对指定的字符串进行签名。
//     *
//     * <p>待签名的字符串 {@code s} 按照 {@code charset} 编码为字符数组，然后通过算法 {@code algorithm}和密钥 {@code key}进行签名。</p>
//     *
//     * @param key
//     *         用于签名的私钥。
//     * @param algorithm
//     *         签名算法。签名算法应当与私钥匹配，比如私钥采用RSA算法，签名采用SHA1-RSA算法。
//     * @param content
//     *         需要签名的文本
//     * @param charset
//     *         将文本转化为字节的编码。
//     * @return 签名结果, 如果需要签名的文本是空字符串, 那么返回长度为0的数组。
//     * @throws IllegalArgumentException
//     *         如果参数{@code key}或者{@code algorithm}或者{@code content}或者{@code charset}是{@code null}。
//     * @throws IllegalArgumentException
//     *         如果参数{@code algorithm}是{@code null}或者只包含空白字符。
//     * @see #verify(PublicKey, String, String, Charset, byte[])
//     */
//    public static byte[] sign(
//            PrivateKey key,
//            String algorithm,
//            String content,
//            Charset charset
//    ) {
//        notNull(key, "key");
//        notBlank(algorithm, "algorithm");
//        notNull(content, "content");
//        notNull(charset, "charset");
//
//        if (content.isEmpty()) {
//            return new byte[0];
//        }
//
//        try {
//            final Signature signer = Signature.getInstance(algorithm);
//
//            signer.initSign(key);
//
//            int p = 0;
//            final int l = content.length();
//            final int lr = l - MAX_DIGEST_BLOCK_SIZE;
//            while (p < lr) {
//                final int pe = p + MAX_DIGEST_BLOCK_SIZE;
//                signer.update(content.substring(p, pe).getBytes(charset));
//                p = pe;
//            }
//            if (p < l) {
//                signer.update(content.substring(p).getBytes(charset));
//            }
//
//            return signer.sign();
//        } catch (GeneralSecurityException ex) {
//            throw new IllegalStateException(ex);
//        }
//    }
//
//    /**
//     * 对指定的字符串验证签名。
//     *
//     * <p>待验证的字符串 {@code s} 按照 {@code charset} 编码为字符数组，然后通过算法 {@code algorithm}和密钥 {@code key}验证是否和给定的签名 {@code signature}匹配。</p>
//     *
//     * @param key
//     *         用于签名的私钥。
//     * @param algorithm
//     *         签名算法。签名算法应当与私钥匹配，比如私钥采用RSA算法，签名采用SHA1-RSA算法。
//     * @param content
//     *         需要签名的文本
//     * @param charset
//     *         将文本转化为字节的编码。
//     * @return 如果签名匹配则返回{@code true}，否则返回{@code false}。如果参数 {@code content}的值是空字符串，那么此签名应当是长度为0的数组。
//     * @throws IllegalArgumentException
//     *         如果参数{@code key}或者{@code algorithm}或者{@code content}或者{@code charset}或者{@code signature}是{@code null}。
//     * @throws IllegalArgumentException
//     *         如果参数{@code algorithm}是{@code null}或者只包含空白字符。
//     * @see #sign(PrivateKey, String, String, Charset)
//     */
//    public static boolean verify(
//            PublicKey key,
//            String algorithm,
//            String content,
//            Charset charset,
//            byte[] signature
//    ) {
//        notNull(key, "key");
//        notBlank(algorithm, "algorithm");
//        notNull(content, "content");
//        notNull(charset, "charset");
//        notNull(signature, "signature");
//
//        if (content.isEmpty()) {
//            return signature.length == 0;
//        }
//
//        try {
//            final Signature sign = Signature.getInstance(algorithm);
//
//            sign.initVerify(key);
//
//            int p = 0;
//            final int l = content.length();
//            final int lr = l - MAX_DIGEST_BLOCK_SIZE;
//            while (p < lr) {
//                final int pe = p + MAX_DIGEST_BLOCK_SIZE;
//                sign.update(content.substring(p, pe).getBytes(charset));
//                p = pe;
//            }
//            if (p < l) {
//                sign.update(content.substring(p).getBytes(charset));
//            }
//
//            return sign.verify(signature);
//        } catch (GeneralSecurityException ex) {
//            throw new RuntimeException(ex);
//        }
//    }


    /**
     * 按照指定的扰动因子对指定的字符串进行扰动。
     *
     * @param src
     *         待扰动的字符串。
     * @param salt
     *         扰动因子。
     * @return 扰动后的字符串。扰动因子被嵌入到原字符串的中间, 如果指定的扰动因子是{@code null}或者只包含空白字符, 那么使用默认扰动因子; 如果指定的字符串是
     * {@code null}或者只包含空白字符, 那么返回2个相同扰动因子连接后的字符串。
     * @throws IllegalArgumentException
     *         如果参数{@code salt}是{@code null}或者只包含空白字符。
     */
    public static String disturb(
            String src,
            String salt
    ) {
        notBlank(salt, "salt");

        if (isBlank(src)) {
            return salt + salt;
        }

        final int p = src.length() > 6 ? src.length() / 2 - 2 : 1;
        return src.substring(0, p) + salt + src.substring(p - 1);
    }

//    /**
//     * 生成对称密钥。
//     *
//     * @param algorithmic
//     *         密钥算法，比如 {@literal DES}。
//     * @param keySize
//     *         密钥长度，该参数的值和算法相关。
//     * @return 指定算法的随机生成的密钥。
//     * @throws IllegalArgumentException
//     *         如果参数{@code algorithmic}是{@code null}或者只包含空白字符，或者参数{@code keySize}不符合指定的算法。
//     * @throws GeneralSecurityException
//     *         如果指定的算法不存在或者生成随机密钥出错。
//     */
//    public static SecretKey generateSecretKey(
//            String algorithmic,
//            int keySize
//    )
//            throws GeneralSecurityException {
//        algorithmic = notBlank(algorithmic, "algorithmic").trim().toUpperCase();
//        greaterThan(keySize, "keySize", 0);
//
//        final KeyGenerator g = KeyGenerator.getInstance(algorithmic);
//        // TODO 使用g.init(keySize, SecureRandom)重载指定随机数算法。
//        g.init(keySize);
//        return g.generateKey();
//    }
//
//    /**
//     * 生成非对称密钥对。
//     *
//     * @param algorithmic
//     *         密钥算法，比如 {@literal RSA}。
//     * @param keySize
//     *         密钥长度，该参数的值和算法相关。
//     * @return 指定算法的随机生成的密钥对，包含公钥和私钥。
//     * @throws IllegalArgumentException
//     *         如果参数{@code algorithmic}是{@code null}或者只包含空白字符，或者参数{@code keySize}不符合指定的算法。
//     * @throws GeneralSecurityException
//     *         如果指定的算法不存在或者生成随机密钥出错。
//     */
//    public static KeyPair generateKeyPair(
//            String algorithmic,
//            int keySize
//    )
//            throws GeneralSecurityException {
//        algorithmic = notBlank(algorithmic, "algorithmic").trim().toUpperCase();
//        greaterThan(keySize, "keySize", 0);
//
//        KeyPairGenerator g = KeyPairGenerator.getInstance(algorithmic);
//        // TODO 使用g.init(keySize, SecureRandom)重载指定随机数算法。
//        g.initialize(keySize);
//        return g.generateKeyPair();
//    }
//
//    public static void main(String[] args)
//            throws GeneralSecurityException, IOException {
////        final byte[] key = generateSecretKey("DES", 56).getEncoded();
////        final String rawKey = Base64.getEncoder().encodeToString(key);
////        writeStringToFile(new File("armory.key"), rawKey, UTF_8.name(), false);
//        final KeyPair p = generateKeyPair("RSA", 512);
//        byte[] key = p.getPublic().getEncoded();
//        String rawKey = Base64.getEncoder().encodeToString(key);
//        writeStringToFile(new File("public.key"), rawKey, UTF_8.name(), false);
//        key = p.getPrivate().getEncoded();
//        rawKey = Base64.getEncoder().encodeToString(key);
//        writeStringToFile(new File("private.key"), rawKey, UTF_8.name(), false);
////        byte[] key;
////        do {
////            key = nextBytes(1024 / 8);
////        } while (DESKeySpec.isWeak(key, 0));
////
////        final String rawKey = getEncoder().encodeToString(key);
////
////        final String keyFileName;
////        if (args.length < 1) {
////            keyFileName = "armory-sec.key";
////        } else {
////            keyFileName = args[0];
////        }
////        writeStringToFile(new File(keyFileName), rawKey, UTF_8.name(), false);
//    }
}
